Skip to content

RTL8814AU: port PHY_SetTxPowerIndex_8814A (BB 0x1998 packed write)#26

Merged
josephnef merged 1 commit into
masterfrom
feat/8814au-txpower-table-port
May 22, 2026
Merged

RTL8814AU: port PHY_SetTxPowerIndex_8814A (BB 0x1998 packed write)#26
josephnef merged 1 commit into
masterfrom
feat/8814au-txpower-table-port

Conversation

@josephnef
Copy link
Copy Markdown
Collaborator

Summary

  • Port PHY_SetTxPowerIndex_8814A from upstream hal/rtl8814a/rtl8814a_phycfg.c:743 — the 8814 TX-power-table uses a single packed DWord write to BB register 0x1998 per (path, rate), totally different from 8812's per-byte per-rate register layout (rTxAGC_A_CCK*, rTxAGC_A_Ofdm*, etc.).
  • The previous code path was writing 8812 register addresses on 8814; those registers don't exist there, so the writes scribbled random bits and the chip's BB stalled on each one — the full TX-power loop took ~6 minutes to finish on 8814, which is why an earlier workaround skipped it entirely in monitor mode.
  • Branched at the top of PHY_SetTxPowerIndex_8812A on CHIP_8814A; existing 8812 fanout below is untouched. Also extends phy_set_tx_power_level_by_path to cover HT_MCS16_MCS23 and VHT_3SSMCS0_3SSMCS9 for 8814 (upstream PHY_SetTxPowerLevel8814 populates all rate sections regardless of link capability).

Background

The fix matches upstream verbatim:

u32 txagc_table_wd = 0x00801000;
txagc_table_wd |= (RFPath << 8) | MRateToHwRate(Rate) | (PowerIndex << 24);
phy_set_bb_reg(Adapter, 0x1998, bMaskDWord, txagc_table_wd);
if (Rate == MGN_1M)
    phy_set_bb_reg(Adapter, 0x1998, bMaskDWord, txagc_table_wd);

The previous "skip TX power on 8814 in monitor mode" workaround landed in 8814AU RX WORKING: skip TX power setup in monitor mode + log packets is removed; gating the loop with DEVOURER_SKIP_TXPWR=1 remains as an escape hatch.

What this does NOT fix

End-to-end 8814 TX. With this change, init completes fast and the TX-power table is correctly programmed, but bulk OUT EP 0x02 still times out (rc=-7) post-init — chip-init leaves the USB controller in a state where EP 0x02 doesn't drain, for a different reason. That gate is tracked separately and will be a follow-up PR.

Test plan

  • Build green on macOS (cmake --build build)
  • Build green on Arch Linux 6.18 (trainer-arch box)
  • 8814 RX regression on CF-938AC (0bda:8813), channel 6: 10+ packets received in demo window (was previously working via the skip workaround; still works after this change)
  • Init completes in ~5s on 8814 (vs prior ~6 minutes when the loop ran on the broken 8812 register format)
  • 8812 RX regression — code path is gated by CHIP_8814A so the 8812 path is untouched, but ideally smoke-tested before merge

🤖 Generated with Claude Code

8814AU uses a fundamentally different TX-power-table register layout
than 8812: a single packed DWord write to BB register 0x1998 per
(path, rate), where the value encodes RF-path, rate code, and power
index. The 8812 path writes to per-rate per-byte registers
(rTxAGC_A_CCK11_CCK1_JAguar, rTxAGC_A_Ofdm18_Ofdm6_JAguar, etc.) that
don't exist on 8814; those writes scribbled random bits and stalled
the chip's BB on each one, which is why the full TX-power loop took
~6 minutes to complete on 8814 — and why an earlier workaround skipped
it entirely on 8814 in monitor mode.

This change ports PHY_SetTxPowerIndex_8814A from upstream
hal/rtl8814a/rtl8814a_phycfg.c:743 verbatim:

  txagc_table_wd[31:24] = PowerIndex
  txagc_table_wd[15:8]  = RFPath
  txagc_table_wd[7:0]   = MRateToHwRate(Rate)
  txagc_table_wd       |= 0x00801000  (TXAGC table-write enable + addr)

  phy_set_bb_reg(0x1998, bMaskDWord, txagc_table_wd);
  if (Rate == MGN_1M) phy_set_bb_reg(...);  // first time turns on table

Branched at the top of PHY_SetTxPowerIndex_8812A on CHIP_8814A. The
existing 8812 register fanout below stays untouched. Also extends
phy_set_tx_power_level_by_path to cover HT_MCS16_MCS23 and
VHT_3SSMCS0_3SSMCS9 for 8814 — upstream PHY_SetTxPowerLevel8814
iterates all sections so the TXAGC table is fully populated even
though the USB-2 link can't sustain 3-SS rates.

The previous "skip TX power on 8814 in monitor mode" workaround is
removed; gating the loop with DEVOURER_SKIP_TXPWR=1 remains as an
escape hatch.

Verified on CF-938AC (0bda:8813, channel 6):
  - Init completes in ~5s (vs the prior 6-minute stall).
  - 8814 RX still works end-to-end (10+ packets received in demo
    window).

Note: this does NOT fix end-to-end 8814 TX. bulk OUT EP 0x02 still
times out (rc=-7) post-init for an independent reason — chip-init
puts the USB controller in a state where EP 0x02 doesn't drain,
even though TX power is now correctly programmed. That gate is
tracked separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@josephnef josephnef merged commit adea8b7 into master May 22, 2026
5 checks passed
@josephnef josephnef deleted the feat/8814au-txpower-table-port branch May 22, 2026 11:48
josephnef added a commit that referenced this pull request May 22, 2026
## Summary

Two changes that together let an 8814AU chip actually transmit on-air
under devourer's monitor-mode injection path:

### 1. TX descriptor byte-identical to kernel-driver

Verified by usbmon capture of an aircrack-ng/morrownr 8814au
kernel-driver session injecting a probe-request frame on the same chip
and channel, diffed against devourer's descriptor. Seven fields
differed:

| Field | Was | Now | Rationale |
|---|---|---|---|
| `MACID` | 0 | 1 | broadcast/default CAM |
| `RATE_ID` (non-VHT) | 7 | 8 | rate-table index |
| `GID` | 0 | 63 (`0x3F`) | no-group default |
| `SW_DEFINE` | 0 | 1 | `DriverFixedRate` flag |
| `RETRY_LIMIT_ENABLE` | 0 | 1 | mgmt-frame default |
| `DATA_RETRY_LIMIT` | 0 | 12 | upstream `rtl8814au_xmit.c:267` |
| `SPE_RPT` | 1 | 0 | kernel does not set |
| `DISABLE_FB` | 1 | 0 | kernel does not set |

Devourer's first TX bulk-OUT now reads `64002885 01120800 0000003f
00010000 00003200 00000000 01000000 76a90000` — byte-identical to the
kernel-driver's TX descriptor.

### 2. Opt-in `DEVOURER_OOT_REPLAY=1`

Runs a verbatim replay of the kernel-driver's post-fwdl vendor-write
sequence (4464 writes between the last fwdl bulk chunk and first TX bulk
OUT, captured via usbmon) at end of init.

Devourer's HAL init even after PRs #25/#26/#27 leaves the chip in a
state that diverges from the kernel-driver in many small ways which
combine to wedge the chip's USB controller — bulk OUT EP 0x02 NAKs every
TX URB. With the replay applied, devourer's chip-state matches the
kernel byte-for-byte (verified via live pyusb register dump) and TX URBs
drain.

**Authoritative usbmon capture, 5-second steady-state TX window:**

```
140-byte bulk OUT submitted:    566
completed status=0:             566
completed status<0:               0
```

(Repeatable across multiple runs.)

With replay disabled (default), bulk OUT continues to time out at the
500ms `USB_TIMEOUT` — unchanged behaviour vs prior master.

### Why opt-in and not default-on

The replay's BB writes significantly slow the chip's RX throughput
(RX-packet rate drops ~10× in a 60-second window). The trade-off is
acceptable for TX-only workloads (injection-only monitor mode); RX-only
users keep current behaviour by leaving the env var unset.

### Long-term path

Replace the verbatim replay by porting the equivalent upstream init
functions individually (`rtl8814a_hal_init.c` + `usb_halinit.c`) so TX
works without the RX trade-off and without 130 KB of opaque trace data
shipped in the binary. The verbatim replay is the minimum that actually
unblocks TX today and serves as a regression checkpoint while the
functions get ported.

## How to use

```bash
# 8814AU TX from monitor mode:
sudo DEVOURER_PID=0x8813 DEVOURER_CHANNEL=6 DEVOURER_OOT_REPLAY=1 \
  ./build/WiFiDriverTxDemo
```

## Verification done

- [x] Build green on macOS + Arch Linux 6.18
- [x] Default (no env var): 8814 RX unchanged from master
(`WiFiDriverDemo` on `0bda:8813`)
- [x] `DEVOURER_OOT_REPLAY=1`: bulk OUT URBs complete `status=0` from
the chip (usbmon-verified across multiple runs)
- [x] TX descriptor byte-identical to kernel-driver TX (usbmon-verified)
- [x] Live pyusb register dump confirms chip state matches kernel-driver
byte-for-byte at all 23 addresses previously diverging

## Not verified

On-air sniffer verification was not possible in the current lab setup —
the aircrack-ng 88XXau OOT driver needed for the 8812 sniffer fails to
build against kernel 6.18. The combined evidence (usbmon-verified URB
completions + byte-identical chip-state + byte-identical descriptor as a
known-working kernel-driver TX session) supports the end-to-end TX
claim, but air-side verification on a receiving adapter is a follow-up.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant